<?php
declare(strict_types=1);

namespace App\Utils\Other\Payment\Neopay;

use App\Exception\InternalException;
use App\Utils\Other\Payment\Base;
use Hyperf\Collection\Arr;
use App\Utils\Http\Request;
use Hyperf\Context\ApplicationContext;
use Psr\SimpleCache\CacheInterface;
use function App\Utils\Common\NeopayRsa\generate;
use function Hyperf\Support\make;
use function Hyperf\Translation\__;
use function App\Utils\Common\NeopayRsa\verify;
use function App\Utils\Common\NeopayRsa\decrypt;
use function App\Utils\Common\Aes\decrypt as decryptAes;
use function App\Utils\Common\NeopayRsa\encryptByPublicKey;
use function App\Utils\Common\NeopayRsa\sign;

/**
 * Class App
 * @package App\Utils\Other\Payment\Neopay
 */
abstract class App extends Base {

    /**
     * 获取AccessToken和公钥
     * @param bool $isRefresh
     * @return array
     * @throws \GuzzleHttp\Exception\GuzzleException
     * @throws \Psr\Container\ContainerExceptionInterface
     * @throws \Psr\Container\NotFoundExceptionInterface
     * @throws \Psr\SimpleCache\InvalidArgumentException
     */
    protected function accessToken(bool $isRefresh=false): array
    {
        $cache = ApplicationContext::getContainer()->get(CacheInterface::class);
        $accessTokenKey = $this->getConfig('mid').'_neopay_access_token';
        $ysPublicCacheKey = $this->getConfig('mid').'_neopay_ys_public_key';
        if (false === $isRefresh && $cache->has($accessTokenKey)) {
            return [
                $cache->get($accessTokenKey),
                $cache->get($ysPublicCacheKey)
//                $this->getConfig('public_cert_path')
            ];
        }

        $params = [
            'data' => [
                'loginId' => $this->getConfig('login_id'),
                'password' => $this->getConfig('login_password'),
            ]
        ];

        $path = $this->getConfig('path');
        $this->setConfig(['path'=>'api/merchants/login']);
        [,$result,] = $this->request($params);
        $this->setConfig(['path'=>$path]);

        $token = $result['encryptInfo']['accessToken'];
        $ysPublicKey = $result['encryptInfo']['ysPublicKey'];
        if (!str_starts_with($ysPublicKey, '-----')) {
            $cert = chunk_split($ysPublicKey, 64, "\n");
            $ysPublicKey = "-----BEGIN PUBLIC KEY-----\n".$cert.'-----END PUBLIC KEY-----';
        }
        $cache->set($accessTokenKey, $token, 29 * 86400);
        $cache->set($ysPublicCacheKey, $ysPublicKey, 29 * 86400);
        return [$token, $ysPublicKey /*$this->getConfig('public_cert_path')*/];
    }

    /**
     * 请求
     * @param array $params
     * @return array
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    protected function request(array $params = []): array
    {

        // TODO: Implement request() method.
        /** @var Request $request */
        $request = make(Request::class);

        $params = $this->params($params);
        $response = $request->post(
            $this->getConfig('url').$this->getConfig('path'),
            $params,
            true
        );

        return $this->response($response, $params);
    }

    /**
     * 参数处理
     * @param array $params
     * @return array
     * @throws \Exception
     */
    protected function params(array $params = []): array
    {
        $mid = $this->getConfig('mid');
        $headers = [
            'merchantNumber' => $mid,
            'trace' => $this->getTrace($mid),
            'version' => '1.0',
        ];

        $merge = array_merge($headers, $params);

        $body = [
            'merchantNumber' => $merge['merchantNumber'],
            'trace' => $merge['trace'],
            'version' => $merge['version']
        ];
        Arr::has($merge, 'encryptInfo') && $body['encryptInfo'] = Arr::get($merge, 'encryptInfo');
        Arr::has($merge, 'data') && $body['data'] = Arr::get($merge, 'data');

        logs('body:'. json_encode($body, JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES));
        if (Arr::has($body, 'encryptInfo')) {
            [,$publicKey] = $this->accessToken();
            $body['encryptInfo'] = encryptByPublicKey(
                json_encode($body['encryptInfo'], JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES),
                $publicKey
            );

            $sign = sign(
                json_encode($body, JSON_UNESCAPED_UNICODE + JSON_UNESCAPED_SLASHES),
                $this->getConfig('private_cert_path')
            );
            $body['signature'] = base64_encode($sign);
        }

        return $body;
    }

    /**
     * 获取追踪号
     * @param string $mid
     * @return string
     * @throws \Exception
     */
    protected function getTrace(string $mid): string
    {
        return $mid.date('YmdHis').generate(6);
    }

    /**
     * 响应处理
     * @param $response
     * @param array $params
     * @return array
     */
    protected function response($response, array $params = []): array
    {
        // TODO: Implement response() method.
        logs($response);
        if (!$response) {
            throw new InternalException(__('request.nobody'));
        }
        $result = json_decode($response, true);
        $jsonError = json_last_error();
        if ($jsonError !== JSON_ERROR_NONE) {
            throw new InternalException(__('request.json_error', [
                'code' => $jsonError,
                'message' => json_last_error_msg()
            ]));
        }

        if ($this->isNeedDecrypt()) {
            $result = $this->decrypt($result);
        }

        if (!hash_equals('00', Arr::get($result, 'code'))) {
            throw new InternalException(__('request.error', [
                'code' => Arr::get($result, 'code'),
                'message' => Arr::get($result, 'message')
            ]));
        }

        return [
            $params, $result, Arr::get($result, 'data'),
            $this->getConfig('url').$this->getConfig('path')
        ];
    }

    /**
     * 是否需要解密
     * @return bool
     */
    protected function isNeedDecrypt(): bool
    {
        $isNeedPathArr = [
            'register', 'login', 'merchants/private_persons/create',
            'file/upload', 'messages/send'
        ];

        foreach ($isNeedPathArr as $path) {
            if (str_contains($this->getConfig('path'), $path)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 解密处理
     * @param array $result
     * @return array
     */
    protected function decrypt(array $result): array
    {
        // TODO: Implement decrypt() method.
        // 获取签名
        $signature = base64_decode($result['signature']);
        // 获取商户公钥
        [, $publicCertPath] = $this->accessToken();
        // RSA签名解密
        $requestStr = verify($signature, $publicCertPath);
        unset($result['signature']);
        $sign = json_encode($result, JSON_UNESCAPED_SLASHES + JSON_UNESCAPED_UNICODE);
        $verify = strcmp($sign, $requestStr) === 0;
        if (!$verify) {
            throw new InternalException(__('message.sign.verify'));
        }
        // 商户私钥
        $privateCertPath = $this->getConfig('private_cert_path');
        if (Arr::has($result, 'encryptInfo')) {
            // 解密
            $dgtlEnvlP = decrypt(base64_decode($result['encryptInfo']['dgtlEnvlp']), $privateCertPath);
            $encryptInfo = decryptAes($dgtlEnvlP, base64_decode($result['encryptInfo']['encryptInfo']));
            $result['encryptInfo']['dgtlEnvlp'] = $dgtlEnvlP;
            $result['encryptInfo']['encryptInfo'] = $encryptInfo;
        }

        logs("RESULT: ");
        logs($result);
        return $result;
    }
}